目录
  1. 1. 前言
  2. 2. Demo 1
  3. 3. Demo 2
  4. 4. 分析与总结
.fini_array段劫持

参考链接:

https://www.lhyerror404.cn/2019/12/19/fini_array%e6%ae%b5%e5%8a%ab%e6%8c%81/

https://r0co.top/passages/%E5%88%A9%E7%94%A8LD-PRELOAD-HOOK%E7%B3%BB%E7%BB%9F%E5%86%85%E7%BD%AE%E5%87%BD%E6%95%B0/

https://www.freebuf.com/articles/web/192052.html

https://blog.csdn.net/chen_jianjian/article/details/80627693

前言

大多数可执行文件是通过链接 libc 来进行编译的,因此 gcc 会将 glibc 初始化代码放入编译好的可执行文件和共享库中。 .init_array和 .fini_array 节(早期版本被称为 .ctors和 .dtors )中存放了指向初始化代码和终止代码的函数指针。 .init_array 函数指针会在 main() 函数调用之前触发。这就意味着,可以通过重写某个指向正确地址的指针来将控制流指向病毒或者寄生代码。 .fini_array 函数指针在 main() 函数执行完之后才被触发,在某些场景下这一点会非常有用。例如,特定的堆溢出漏洞(如曾经的 Once upon a free())会允许攻击者在任意位置写4个字节,攻击者通常会使用一个指向 shellcode 地址的函数指针来重写.fini_array 函数指针。对于大多数病毒或者恶意软件作者来说, .init_array 函数指针是最常被攻击的目标,因为它通常可以使得寄生代码在程序的其他部分执行之前就能够先运行。

Demo 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdlib.h>


static void start(void) __attribute__ ((constructor));
static void stop(void) __attribute__ ((destructor));


int main(int argc, char *argv[])
{
printf("start == %p\n", start);
printf("stop == %p\n", stop);
return 0;
}


void start(void)
{
printf("hello world!\n");
}


void stop(void)
{
printf("goodbye world!\n");
}

gcc为函数提供了几种类型的属性,其中两个是我们特别感兴趣的:构造函数(constructors)和析构函数(destructors)。程序员应当使用类似下面的方式来指定这些属性:

1
2
static void start(void) __attribute__ ((constructor));
static void stop(void) __attribute__ ((destructor));

带有”构造函数”属性的函数将在main()函数之前被执行,而声明为”析构函数”属性的函数则将在after main()退出时执行。

程序运行结果如下:

1
2
3
4
5
6
7
8
➜  fini_array gcc test.c -o test
➜ fini_array ls
test test.c
➜ fini_array ./test
hello world!
start == 0x4005a4
stop == 0x4005b5
goodbye world!

下载我们试试 objdump -h ./test

1
2
3
4
5
➜  fini_array objdump -h ./test
18 .init_array 00000010 0000000000600e00 0000000000600e00 00000e00 2**3
CONTENTS, ALLOC, LOAD, DATA
19 .fini_array 00000010 0000000000600e10 0000000000600e10 00000e10 2**3
CONTENTS, ALLOC, LOAD, DATA

可以看到.init_array的地址为 0x600e00 , .fini_array的地址为 0x600e10

在gdb中分别对这两个地址跟踪一下

1
2
3
pwndbg> x/4xg 0x600e00
0x600e00: 0x0000000000400540 0x00000000004005a4
0x600e10: 0x0000000000400520 0x00000000004005b5

分析一下结果
.init_array存的 0x400540是 frame_dummy函数地址(ida里面可查看) 0x4005a4很明显是自己定义的start函数的地址

.fini_array存的 0x400540是 __do_global_dtors_aux函数地址(ida里面可查看) 0x4005b5 很明显是定义的stop函数的地址

Demo 2

我们再看一个例子,其实就是前面的test程序函数少了属性,我把它定义成静态函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <stdlib.h>


static void start(void);
static void stop(void);


int main(int argc, char *argv[])
{
printf("start == %p\n", start);
printf("stop == %p\n", stop);

return 0;
}


void start(void)
{
printf("hello world!\n");
}


void stop(void)
{
printf("goodbye world!\n");
}

同样编译和运行:

1
2
3
4
➜  fini_array gcc test2.c -o test
➜ fini_array ./test
start == 0x4005a4
stop == 0x4005b5

函数地址并没有变化,但是因为start/stop函数未设定析构与构造属性,所以没有在开始和结束时被调用。

我们试试 objdump -h ./test2

1
2
3
4
5
➜  fini_array objdump -h ./test2
18 .init_array 00000008 0000000000600e10 0000000000600e10 00000e10 2**3
CONTENTS, ALLOC, LOAD, DATA
19 .fini_array 00000008 0000000000600e18 0000000000600e18 00000e18 2**3
CONTENTS, ALLOC, LOAD, DATA

可以看到.init_array的地址为 0x600e10 , .fini_array的地址为 0x600e18,和test程序有点偏差。

现在我用gdb跟踪一波,查看一下.fini_array

1
2
pwndbg> x/2xg 0x600e18
0x600e18: 0x0000000000400530 0x0000000000000000

明显0x0000000000400530后面的函数指针没有被填充 是0x0000000000000000,所以程序结束后不会执行stop函数

现在我们控制程序执行流程,怎么控制呢?我把.fini_array的函数指针0x0000000000400530覆盖成stop函数的地址

1
2
3
pwndbg> set {int}0x600e18=0x4005b5
pwndbg> x/2xg 0x600e18
0x600e18: 0x00000000004005b5 0x0000000000000000

输入c继续执行程序

1
2
3
4
5
6
pwndbg> c
Continuing.
start == 0x4005a4
stop == 0x4005b5
goodbye world!
[Inferior 1 (process 7442) exited normally]

成功执行了stop函数,如果stop函数是一段onegadget或shellcode我们就可以直接拿下shell

分析与总结

我们来关心一下,上面的stop在什么地方被调用。

栈回溯跟踪看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 ► 0x4005b5 <stop>                 push   rbp
0x4005b6 <stop+1> mov rbp, rsp
0x4005b9 <stop+4> mov edi, 0x40067a
0x4005be <stop+9> call puts@plt <0x400430>

0x4005c3 <stop+14> nop
0x4005c4 <stop+15> pop rbp
0x4005c5 <stop+16> ret

0x4005c6 nop word ptr cs:[rax + rax]
0x4005d0 <__libc_csu_init> push r15
0x4005d2 <__libc_csu_init+2> push r14
0x4005d4 <__libc_csu_init+4> mov r15d, edi
───────────────────────────────────────[ STACK ]───────────────────────────────────────
00:0000│ rsp 0x7fffffffdc68 —▸ 0x7ffff7de7df7 (_dl_fini+823) ◂— test r13d, r13d
01:0008│ r14 0x7fffffffdc70 —▸ 0x7ffff7ffe168 ◂— 0x0
02:0010│ 0x7fffffffdc78 —▸ 0x7ffff7ffe700 —▸ 0x7ffff7ffa000 ◂— jg 0x7ffff7ffa047
03:0018│ 0x7fffffffdc80 —▸ 0x7ffff7fb5000 —▸ 0x7ffff7a0d000 ◂— jg 0x7ffff7a0d047
04:0020│ r10 0x7fffffffdc88 —▸ 0x7ffff7ffd9d8 (_rtld_global+2456) —▸ 0x7ffff7dd7000 ◂— jg 0x7ffff7dd7047
05:0028│ 0x7fffffffdc90 —▸ 0x7fffffffdd60 —▸ 0x7fffffffde50 ◂— 0x1
06:0030│ 0x7fffffffdc98 —▸ 0x7ffff7de7b44 (_dl_fini+132) ◂— mov ecx, dword ptr [r12]
07:0038│ 0x7fffffffdca0 —▸ 0x7fffffffdc70 —▸ 0x7ffff7ffe168 ◂— 0x0
─────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────
► f 0 4005b5 stop
f 1 7ffff7de7df7 _dl_fini+823
f 2 7ffff7a46ff8 __run_exit_handlers+232
f 3 7ffff7a47045
f 4 7ffff7a2d837 __libc_start_main+247
Breakpoint *0x4005b5
Breakpoint *0x4005B5

看到返回地址在_dl_fini+823,所以可以得出结论,.fini_array区节的第一个函数指针在程序结束时,由_dl_fini函数调用,所以我们可加以利用。在未开启PIE的情况下,只需实现一个任意地址写,将.fini_array区节的第一个函数指针改写成后门地址或者one_gadgets,在程序结束时便能控制流程

文章作者: nocbtm
文章链接: https://nocbtm.github.io/2020/02/20/–-fini-array段劫持/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 nocbtm's Blog
打赏
  • 微信
  • 支付宝